在JavaScript中4种创建枚举方式

您所在的位置:网站首页 Java 枚举类型 在JavaScript中4种创建枚举方式

在JavaScript中4种创建枚举方式

2023-05-20 22:01| 来源: 网络整理| 查看: 265

本文译者为360奇舞团前端开发工程师 原文标题:4 Ways to Create an Enum in JavaScript 原文作者:Dmitri Pavlutin 原文链接:dmitripavlutin.com/javascript-…

在JavaScript中4种创建枚举方式

使用枚举(enum)可以方便地表示一个变量,从一个有限的预定义常量集合中获取值,枚举可以避免使用魔法数字和字符串(这被认为是一种反模式)。

让我们看看在 JavaScript 中创建枚举的四种方法(以及它们的优缺点)。

1. 基于普通对象的枚举

枚举是一种数据结构,它定义了一组有限的命名常量。每个常量可以通过其名称访问。

定义一个T-shirt的size为:Small、Medium、Large。

在JavaScript中创建枚举的一种简单的方式,是使用普通JavaScript对象

const Sizes = { Small: 'small', Medium: 'medium', Large: 'large', } const mySize = Sizes.Medium console.log(mySize === Sizes.Medium) // logs true

Sizes是一个基于普通JavaScript对象的枚举,它有3个命名的常量: Sizes.Small、 Sizes.Medium 、Sizes.Large。

Sizes 同时也是一个字符串枚举,命名的常量的值是字符串: 'small', 'medium' 、'large'。

T_shirt_size

要访问命名的常量值,请使用属性访问器。例如: Sizes.Medium的值是'medium'。

枚举的可读性更强,更明确,并消除了对魔法字符串或数字的使用。

优点和缺点

这种使用普通对象的枚举方法非常简单明了,只要定义一个带有键和值的对象,就可以创建一个枚举。

在大型项目中,有人可能会无意间修改枚举对象,这将影响应用程序的运行时。由于枚举对象是可变的,因此开发人员无法将其设置为不可变。这是普通对象枚举的主要缺点。

const Sizes = { Small: 'small', Medium: 'medium', Large: 'large', } const size1 = Sizes.Medium const size2 = Sizes.Medium = 'foo' // Changed! console.log(size1 === Sizes.Medium) // logs false

在上述代码中Sizes.Medium 枚举值被意外的修改了,size1在初始化时为Sizes.Medium, 不再和Sizes.Medium 相等!普通对象的实现无法防止这种意外更改。

下面让我们看看字符串和符号枚举。以及如何冻结枚举对象以避免意外更改的问题。

2.枚举值类型

除了字符串类型外,枚举的值也可以是number类型:

const Sizes = { Small: 0, Medium: 1, Large: 2 } const mySize = Sizes.Medium console.log(mySize === Sizes.Medium) // logs true

上例中的Sizes枚举是一个数字枚举,因为其值是数字: 0、1、2。

同时也可以创建一个符号(symbol)枚举:

const Sizes = { Small: Symbol('small'), Medium: Symbol('medium'), Large: Symbol('large') } const mySize = Sizes.Medium console.log(mySize === Sizes.Medium) // logs true

使用符号的好处是,每一个符号都是唯一的,这意味着您必须始终使用枚举本身来比较枚举:

const Sizes = { Small: Symbol('small'), Medium: Symbol('medium'), Large: Symbol('large') } const mySize = Sizes.Medium console.log(mySize === Sizes.Medium) // logs true console.log(mySize === Symbol('medium')) // logs false

使用符号枚举的缺点是 JSON.stringify() 将符号序列化为 null、undefined,或者跳过包含符号值的属性,这将导致在需要将枚举转换为字符串形式的情况下产生问题。因此,符号枚举的使用可能需要在其他位置进行修改以支持 JSON.stringify()。

const Sizes = { Small: Symbol('small'), Medium: Symbol('medium'), Large: Symbol('large') } const str1 = JSON.stringify(Sizes.Small) console.log(str1) // logs undefined const str2 = JSON.stringify([Sizes.Small]) console.log(str2) // logs '[null]' const str3 = JSON.stringify({ size: Sizes.Small }) console.log(str3) // logs '{}'

如果你可以自由选择枚举值的类型,就选择字符串吧。字符串比数字和符号更容易进行调试。

3.基于Object.freeze()的枚举

可以保护枚举对象免受修改的一种好方法是将其冻结。当对象被冻结时,您无法修改或添加新属性到该对象中。换句话说,该对象变为只读。

在JavaScript中,Object.freeze() 函数冻结一个对象。让我们冻结Sizes枚举:

const Sizes = Object.freeze({ Small: 'small', Medium: 'medium', Large: 'large', }) const mySize = Sizes.Medium console.log(mySize === Sizes.Medium) // logs true

const Sizes = Object.freeze({ ... }) 创建一个被冻结的对象. 即使被冻结,也可以自由的访问这些枚举值:const mySize = Sizes.Medium 。

优点和缺点

如果一个枚举属性被意外的修改,JavaScript会抛出一个错误(在严格模式下)

const Sizes = Object.freeze({ Small: 'Small', Medium: 'Medium', Large: 'Large', }) const size1 = Sizes.Medium const size2 = Sizes.Medium = 'foo' // throws TypeError

语句 const size2 = Sizes.Medium = 'foo' 对 Sizes.Medium 属性进行了意外的赋值操作。因为Sizes 是一个被冻结的对象,JavaScript(在严格模式下)抛出一个错误:

TypeError: Cannot assign to read only property 'Medium' of object

冻结的对象枚举被保护起来,这可以防止无意中修改枚举对象。

还有一个问题。如果无意间拼写错了枚举常量,结果会变成undefined:

const Sizes = Object.freeze({ Small: 'small', Medium: 'medium', Large: 'large', }) console.log(Sizes.Med1um) // logs undefined

在上面的示例中Sizes.Med1um表达式(Med1um是Medium的错误拼写)的结果将会是undefined。

下面让我们看看如何使用代理实现的枚举可以解决这个问题。

4. 基于代理的枚举

代理枚举是一种有趣的实现方式,也是我最喜欢的一种方式。一个代理对象是一个特殊的对象,它包装另一个对象并修改对原始对象的操作的行为。代理不会改变原始对象的结构。

枚举代理拦截枚举对象的读取和写入操作,并且:

当访问不存在的枚举值时抛出错误 当更改枚举对象属性时抛出错误

下面是一个工厂函数的实现,该函数接受一个普通的枚举对象,并返回一个代理对象:

// enum.js export function Enum(baseEnum) { return new Proxy(baseEnum, { get(target, name) { if (!baseEnum.hasOwnProperty(name)) { throw new Error(`"${name}" value does not exist in the enum`) } return baseEnum[name] }, set(target, name, value) { throw new Error('Cannot add a new value to the enum') } }) }

代理的get()方法拦截读操作,并在属性名称不存在时抛出错误。set()方法拦截写操作并抛出错误。它旨在保护枚举对象免受写操作的影响。

将Sizes对象枚举包装到代理中:

import { Enum } from './enum' const Sizes = Enum({ Small: 'small', Medium: 'medium', Large: 'large', }) const mySize = Sizes.Medium console.log(mySize === Sizes.Medium) // logs true

代理枚举的使用方式和普通对象枚举完全相同,

优点和缺点

使用基于代理的枚举,可以获得一种既灵活又安全的枚举实现方式。代理枚举不仅可以捕获枚举属性的更改,还可以防止访问不存在的枚举常量。

import { Enum } from './enum' const Sizes = Enum({ Small: 'small', Medium: 'medium', Large: 'large', }) const size1 = Sizes.Med1um // throws Error: non-existing constant const size2 = Sizes.Medium = 'foo' // throws Error: changing the enum

因为枚举中没有定义名为'Med1um'的常量,所以 Sizes.Med1um 会引发错误。由于枚举属性已被更改,因此 Sizes.Medium = 'foo' 会引发错误。

代理枚举的缺点是始终需要导入 Enum 工厂函数并将枚举对象包装在其中。与使用其他实现方式相比,代理枚举可能会带来一些性能损失。代理枚举涉及使用 JavaScript 的代理特性,这可能会使枚举对象的访问速度稍慢一些。因此,在实现枚举时,应该考虑的代码的性能需求。

5. 基于类的枚举

使用 JavaScript 类创建枚举, 基于类的枚举包含一组 静态字段,每个静态字段都代表一个命名的枚举常量。每个枚举常量的值本身就是该类的一个实例。

使用 Sizes 类实现sizes枚举:

class Sizes { static Small = new Sizes('small') static Medium = new Sizes('medium') static Large = new Sizes('large') #value constructor(value) { this.#value = value } toString() { return this.#value } } const mySize = Sizes.Small console.log(mySize === Sizes.Small) // logs true console.log(mySize instanceof Sizes) // logs true

Sizes 是代表枚举的类。枚举常量是类上的静态字段,例如 static Small = new Season('small')。 Sizes 类的每个实例都有一个私有字段 #value,它代表枚举的原始值。由于 Sizes 类的构造函数是私有的,所以不能创建新的枚举实例。这就确保了枚举值的不可变性。

基于类的枚举的一个很好的优点是,在运行时能够使用instanceof操作来确定值是否为枚举。例如,mySize instanceof Sizes评估为true,因为mySize是一个枚举值。

在基于类的枚举中,比较是基于实例的(相对于普通、冻结或代理枚举而言,基于实例的比较更为原始):

class Sizes { static Small = new Sizes('small') static Medium = new Sizes('medium') static Large = new Sizes('large') #value constructor(value) { this.#value = value } toString() { return this.#value } } const mySize = Sizes.Small console.log(mySize === new Sizes('small')) // logs false

mySize(它的值为 Sizes.Small)并不等于 new Sizes('small')。即使 Sizes.Small 和 new Sizes('small') 拥有相同的 #value值,但是它们是不同的对象实例。

优点和缺点

基于类的枚举无法防止覆盖或访问不存在的枚举命名常量。

class Sizes { static Small = new Season('small') static Medium = new Season('medium') static Large = new Season('large') #value constructor(value) { this.#value = value } toString() { return this.#value } } const size1 = Sizes.medium // a non-existing enum value can be accessed const size2 = Sizes.Medium = 'foo' // enum value can be overwritten accidentally

但是,可以通过构造函数内部已创建的实例数来控制新实例的创建。如果创建的实例数超过3个,则抛出一个错误。当然,最好将枚举的实现尽可能简单。枚举旨在成为普通的数据结构。

6. 结论

在 JavaScript 中创建枚举的方法有 4 种。

最简单的方法是使用一个普通的 JavaScript 对象: const MyEnum = { Option1: 'option1', Option2: 'option2', Option3: 'option3' }

普通对象枚举最适合小项目或演示demo。

如果想保护枚举对象不被意外重写,则可以使用被冻结的纯对象: const MyEnum = Object.freeze({ Option1: 'option1', Option2: 'option2', Option3: 'option3' })

冻结的对象枚举在中大型项目中非常适用,可以确保枚举不会被意外修改。

3.代理方法:

export function Enum(baseEnum) { return new Proxy(baseEnum, { get(target, name) { if (!baseEnum.hasOwnProperty(name)) { throw new Error(`"${name}" value does not exist in the enum`) } return baseEnum[name] }, set(target, name, value) { throw new Error('Cannot add a new value to the enum') } }) } import { Enum } from './enum' const MyEnum = Enum({ Option1: 'option1', Option2: 'option2', Option3: 'option3' })

代理枚举适用于中型或大型项目,可以更好地保护枚举不被覆盖或访问不存在的命名常量。代理枚举是我的个人比较喜欢的。

是使用基于类的枚举,其中每个命名常量都是该类的实例,并存储为该类的静态属性: class MyEnum { static Option1 = new Season('option1') static Option2 = new Season('option2') static Option3 = new Season('option3') #value constructor(value) { this.#value = value } toString() { return this.#value } }

如果你喜欢使用类,那么基于类的枚举就可以发挥作用。然而,与冻结或代理枚举相比,基于类的枚举受到的保护较少。

你还知道 JavaScript 中创建枚举的其他方式吗?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3